package com.ifonly.activerecord;

import com.ifonly.activerecord.exceptions.SqlExecuteException;
import com.ifonly.activerecord.helpers.SqlBuildHelper;
import com.ifonly.activerecord.helpers.SqlInfo;
import com.ifonly.common.KeyValue;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author <a href="mailto:ifonlymaster@163.com">ifonly</a>
 * @version 1.0 2015-12-17 16:03
 * @since JDK 1.6
 */
public abstract class Model implements Access, Serializable {
    private static final long serialVersionUID = -6085775253822331526L;
    private static Db db = Db.getInstance();

    private Table table;
    private String tableName;
    private String[] primaryKeys;
    private Map<String, Class<?>> columnAndTypes;

    private Map<String, Object> propertyAndValues = new HashMap<String, Object>();
    private Set<String> modifiedProperties = new HashSet<String>();

    {
        table = db.getTableMapper().getTable(this.getClass());
        tableName = table.getTableName();
        primaryKeys = table.getPrimaryKeys();
        columnAndTypes = table.getColumnAndTypes();
    }

    public Map<String, Object> getPropertyAndValues() {
        return propertyAndValues;
    }

    public Set<String> getColumnNames() {
        return columnAndTypes.keySet();
    }

    public Class<?> getColumnType(String column) {
        return columnAndTypes.get(column);
    }

    @SuppressWarnings("unchecked")
    private KeyValue<String, Object>[] getPrimaryKeyWithValue() {
        KeyValue<String, Object>[] pkColumnWithValues = new KeyValue[primaryKeys.length];
        for (int i = 0; i < primaryKeys.length; i++) {
            String pk = primaryKeys[i];
            KeyValue cv = new KeyValue();
            cv.setKey(pk);
            cv.setValue(propertyAndValues.get(pk));
            pkColumnWithValues[i] = cv;
        }
        return pkColumnWithValues;
    }

    // --------------------------------------------------------- save

    public int save() {
        if (modifiedProperties.isEmpty()) {
            throw new IllegalArgumentException("Please set the value want to save");
        }
        SqlInfo sql = SqlBuildHelper.buildInsertSql(tableName, propertyAndValues, modifiedProperties);

        Connection conn = null;
        try {
            conn = db.getConnection();
            int i = DbUtils.insert(conn, sql.getSql(), sql.getParams());
            modifiedProperties.clear();
            return i;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            db.closeConnection(conn);
        }
    }

    public KeyValue<String, Object>[] saveReturnGeneratedKeys() {
        if (primaryKeys == null || primaryKeys.length == 0) {
            throw new IllegalArgumentException("the table[" + tableName + "] has no primary key");
        }
        if (modifiedProperties.isEmpty()) {
            throw new IllegalArgumentException("Please set the value want to save");
        }
        SqlInfo sql = SqlBuildHelper.buildInsertSql(tableName, propertyAndValues, modifiedProperties);

        Connection conn = null;
        try {
            conn = db.getConnection();
            ResultSet rs = DbUtils.insert(conn, primaryKeys, sql.getSql(), sql.getParams());
            int length = primaryKeys.length;
            @SuppressWarnings("unchecked")
            KeyValue<String, Object>[] columnWithValues = new KeyValue[length];

            for (int i = 0; i < length; i++) {
                String pk = primaryKeys[i];
                Object value = null;
                if (rs.next()) {
                    value = rs.getObject(1);
                }
                columnWithValues[i] = new KeyValue<String, Object>(pk, value);
            }
            modifiedProperties.clear();
            return columnWithValues;
        } catch (SQLException e) {
            throw new SqlExecuteException("execute sql failed", e);
        } finally {
            db.closeConnection(conn);
        }
    }

    public KeyValue<String, Object> saveReturnGeneratedKey() {
        KeyValue<String, Object>[] keyValues = saveReturnGeneratedKeys();
        for (KeyValue<String, Object> keyValue: keyValues){
            this.put(keyValue.getKey(), keyValue.getValue());
        }
        return keyValues[0];
    }

    public Object[] saveReturnGeneratedKeyValues() {
        KeyValue<String, Object>[] keyValues = saveReturnGeneratedKeys();
        int length = keyValues.length;
        Object[] values = new Object[length];
        for (int i = 0; i < length; i++) {
            values[i] = keyValues[i].getValue();
        }
        return values;
    }

    public Object saveReturnGeneratedKeyValue() {
        KeyValue<String, Object>[] keyValues = saveReturnGeneratedKeys();
        return keyValues[0].getValue();
    }

    // --------------------------------------------------------- update

    public int update() {
        if (modifiedProperties.isEmpty()) {
            throw new IllegalArgumentException("Please set the value want to save");
        }
        KeyValue<String, Object>[] primaryKeyWithValues = getPrimaryKeyWithValue();
        Connection connection = null;
        try {
            SqlInfo sql = SqlBuildHelper.buildUpdateSql(tableName, propertyAndValues, modifiedProperties, primaryKeyWithValues);
            connection = db.getConnection();
            int i = DbUtils.update(connection, sql.getSql(), sql.getParams());
            modifiedProperties.clear();
            return i;
        } catch (Exception e) {
            throw new SqlExecuteException("execute sql failed", e);
        } finally {
            db.closeConnection(connection);
        }
    }

    // --------------------------------------------------------- delete

    public int delete() {
        KeyValue<String, Object>[] primaryKeyWithValues = getPrimaryKeyWithValue();
        Connection connection = null;
        try {
            SqlInfo sql = SqlBuildHelper.buildDeleteSql(tableName, primaryKeyWithValues);
            connection = db.getConnection();
            int i = DbUtils.delete(connection, sql.getSql(), sql.getParams());
            modifiedProperties.clear();
            return i;
        } catch (Exception e) {
            throw new SqlExecuteException("execute sql failed", e);
        } finally {
            db.closeConnection(connection);
        }
    }

    // --------------------------------------------------------- find

    @SuppressWarnings("unchecked")
    public static <T> T findByPrimaryKey(Class<? extends Model> clazz, Object pk) {
        return (T) DbUtils.findByPrimaryKey(clazz, pk);
    }

    // --------------------------------------------------------- set

    public void set(String property, Object value) {
        if (!columnAndTypes.containsKey(property)) {
            throw new IllegalArgumentException("The Table[" + tableName + "] not have the column[" + property + "]");
        }

        propertyAndValues.put(property, value);
        modifiedProperties.add(property);
    }

    public void setAll(Map<String, Object> propertyAndValues) {
        //this.propertyAndValues = propertyAndValues;
        Set<Map.Entry<String, Object>> entrySet = propertyAndValues.entrySet();
        for (Map.Entry<String, Object> entry : entrySet) {
            set(entry.getKey(), entry.getValue());
        }
    }

    // --------------------------------------------------------- put

    public void put(String attr, Object value) {
        propertyAndValues.put(attr, value);
    }

    public void putAll(Map<String, Object> attrAndValues) {
        propertyAndValues.putAll(attrAndValues);
    }

    // --------------------------------------------------------- query

    @Override
    public String toString() {
        return "{" +
                "tableName='" + tableName + '\'' +
                ", propertyAndValues=" + propertyAndValues +
                '}';
    }

    // ----------------------------------------------------------------------------- gets
    private Accessor accessor;

    public Accessor getAccessor() {
        synchronized (this) {
            if (accessor == null) {
                accessor = new Accessor(this.propertyAndValues);
            }
        }
        return accessor;
    }

    @Override
    public Object get(String property) {
        return getAccessor().get(property);
    }

    @Override
    public Number getNumber(String property) {
        return getAccessor().getNumber(property);
    }

    @Override
    public String getString(String property) {
        return getAccessor().getString(property);
    }

    @Override
    public String getString(String property, String defaultValue) {
        return getAccessor().getString(property, defaultValue);
    }

    @Override
    public Long getLong(String property) {
        return getAccessor().getLong(property);
    }

    @Override
    public long getLongValue(String property) {
        return getAccessor().getLongValue(property);
    }

    @Override
    public long getLongValue(String property, long defaultValue) {
        return getAccessor().getLongValue(property, defaultValue);
    }

    @Override
    public int getInt(String property) {
        return getAccessor().getInt(property);
    }

    @Override
    public int getInt(String property, int defaultValue) {
        return getAccessor().getInt(property, defaultValue);
    }

    @Override
    public Integer getInteger(String property) {
        return getAccessor().getInteger(property);
    }
}
